組込み現場の「C++」プログラミング 明日から使える徹底入門

高木 信尚(株式会社クローバーフィールド

3.1 多重定義の裏側

多重定義は,すぐにでも導入できる簡単で便利な機能ですが,その仕組みを知っておかないとデバッグが困難になります.また,ソースコードレビューを行っても,正確な動作を把握することができません.多重定義の仕組みは,ある意味,C++の裏側を知るうえで要となる部分です.ここを確実におさえておけば,ほかの仕様の理解はかなり楽になることでしょう.

3.1.1 マングル処理

C++では,何か不具合が発生したときにプログラムを解析しようとして逆アセンブルすると,意味不明な呪文のようなラベル名が現れて驚くことがあります.これは,「1.4.1 Cの資産を利用するためのテクニック」で解説した「C++結合」が原因です.C++結合では,多重定義を解決するために関数の引数情報をラベルに埋め込みます.このように,関数の引数情報をラベルに埋め込めるようにシンボルの変換を行うことを「マングル処理」(Name Mangling)といいます.

次に具体的を示します.

void func();
void func(int arg);

上記の2つの関数をコンパイルしたとき,ある処理系では次のようなラベルが生成されます.

_foo__Fv
_foo__Fi

また,ある処理系では,次のようなラベルが生成されます.

__Z3fooz
__Z3fooi

このように,処理系によってマングル処理の方法は変わりますが,いずれの場合も引数情報が埋め込まれていることは確かです.実際には,引数情報に加えて,その関数が所属する名前空間やクラス名の情報も埋め込まれることになります.

ここでもう1つ注目してほしいのですが,多くの処理系では,マングル処理によって連続した下線(__)を含むシンボルを生成します.そのため,C++では連続した下線を含む識別子は処理系に予約されており,ユーザープログラムで使うことはできません.なお,処理系によっては,連続した下線ではなく,アットマーク(@)やドル記号($)などを使うこともあるようです.

マングル処理によって引数情報を埋め込んだシンボルをラベルに使うことで,名前が同じで引数だけが異なる関数の多重定義を解決することができます.また,関数名のマングル処理によって,多重定義を行っていない場合でも,翻訳単位間で関数宣言に矛盾がある場合はリンクエラーが生じるので,潜在的な不具合を検出することができるようになっています.

ちなみに,型に関するマングル処理が行われるのは関数だけという処理系が多いのですが,外部結合を持つオブジェクト名に対してもマングル処理が行われることがあります.

int a;
long b;

上記の2つのオブジェクト宣言をコンパイルしたとき,ある処理系では次のようなラベルが生成されます.

?a@@3HA
?b@@3JA

オブジェクトの型情報がシンボルに埋め込まれない場合でも,名前空間やクラス名の情報は埋め込まれるので,多かれ少なかれ,シンボルはCよりも複雑になります.デバッグ時に,逆アセンブルリストに呪文のようなシンボルが現れたとき,パニックに陥ってしまわないためにも,マングル処理が行われるということ,そして,どのように行われるかということを把握しておく必要があるでしょう.

3.1.2 多重定義の解決

多重定義に関して注意が必要なのはマングル処理だけではありません.たとえば,次のfunc関数にunsigned int型の実引数を渡した場合,どの関数が呼び出されるか即答できるでしょうか?

int func(int arg);
int func(long arg);
int func(double arg);

一度試してみればすぐにわかりますが,多重定義が曖昧になり,コンパイルエラーが発生してしまいます.たとえ実引数が定数式で,コンパイラが静的に値を決定でき,それがint型の表現範囲に収まっていたとしてもです.

多重定義が解決できずにつねにコンパイルエラーになる場合はまだよいのですが,処理系によって振る舞いが変わる場合もあります.たとえば,unsigned short型の実引数を渡した場合,int型がshort型より大きければint型版が呼び出されますが,int型とshort型のサイズが同じであればコンパイルエラーになってしまいます.このように,多重定義を解決するためのルールはかなり複雑です.その証拠に,C++の規格書であるJIS X3014では,多重定義の解決に関する仕様が11ページ(英文のISO/IEC 14882の場合は14ページ)にわたって詳細に記述されています.

呼び出す際の実引数(演算子の場合はオペランド)に対応した関数や演算子の候補が複数ある場合,実際にどれを呼び出すかは次の基準で解決されます.

  1. 実引数の個数
  2. 実引数の型と候補関数の仮引数の型との合致度合い
  3. (非静的メンバー関数について)オブジェクトと暗黙のオブジェクト仮引数との合致度合い
  4. 候補関数のその他の特性

このうち一番トラブルに遭遇しやすいのは,型の合致度合いにかかわる2ではないかと思います.型の合致度合いは,大まかにいえば,次の優先順位で決まります.

  1. 同じ型
    配列や関数→ポインタ
  2. const/volatile修飾子が付いた型
  3. 汎整数拡張
    浮動小数点拡張
  4. その他の暗黙的型変換
  5. 変換コンストラクタ/変換関数
  6. 省略記号(...)

Cでは,汎整数型のサイズに応じて,int8,int16,int32とか,TRON系であればB,H,Wのような型定義が行われがちです.むしろ多くの場合,そうすることが推奨されます.また,C99では,int8_t,int16_t,int32_tといった型が標準でサポートされるようになりました.しかし,C++においては,型の特性としては,サイズだけではなく,「何型なのか」という静的な型情報が重要になります.多重定義やテンプレートの解決において,静的な型情報は値と同じか,それ以上に重要なのです.

気のきいた設計では,処理系に依存して解釈が変わるような多重定義は,そもそも,行わないか,あるいは処理系に応じて適切な関数が選択されるように意図されています.多重定義は便利ですが,乱用すべきではありません.多重定義された関数の仮引数を何型にするかの選択は,条件選択に関する処理を記述しているのと同じことなのです.